HTTP缓存 - Last-Modified和Last-Modified-Since

前言

  在HTTP中,最常用的GET请求方是可缓存的(cacheable),通俗地说就是通过这种请求方式的资源会被浏览器缓存下来,如果所请求的资源没有任何变动,那么下一次请求就可以访问被缓存的资源,而不需要从服务器重新获取对应的资源。

情景

  创建一个Web应用并把它部署到Tomcat中,假设我们使用Chrome访问某个静态页面a.html,并用开发者工具查看Network这一项,查看请求消息和响应消息。

第一次访问

请求消息

1
2
3
4
5
6
7
8
9
10
11
GET /day04-http/a.html HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4
Cookie: JSESSIONID=A26AF7FA34CE430A21E32D8666B05717; Idea-8752b670=e6037236-ceb5-4357-9600-1e08fca1a591;
_ga=GA1.1.327730390.1505579288; _gid=GA1.1.181028957.1507174466

响应消息

1
2
3
4
5
6
7
HTTP/1.1 200
Accept-Ranges: bytes
ETag: W/"220-1507175918413"
Last-Modified: Thu, 05 Oct 2017 03:58:38 GMT
Content-Type: text/html
Content-Length: 220
Date: Thu, 05 Oct 2017 03:59:51 GMT

请求消息没什么特别之处;响应消息中,状态码是200,对应的状态描述是OK,证明一切正常,而最值得注意的是响应头Last-Modified

1
Last-Modified: Thu, 05 Oct 2017 03:58:38 GMT

这个时间是服务器告诉浏览器该请求资源的最后修改时间
这个时间正是a.html的最后修改时间。默认是格林威治时间,这点我们不用去管,因为在HTTP的请求头和响应头中所有日期相关的都是格林威治时间。

第二次访问

现在,我们千万不要对a.html进行任何变动,就像之前那样直接访问它,就能得到以下信息。

请求消息

1
2
3
4
5
6
7
8
9
10
11
12
13
GET /day04-http/a.html HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4
Cookie: JSESSIONID=A26AF7FA34CE430A21E32D8666B05717; Idea-8752b670=e6037236-ceb5-4357-9600-1e08fca1a591;
_ga=GA1.1.327730390.1505579288; _gid=GA1.1.181028957.1507174466
If-None-Match: W/"220-1507175918413"
If-Modified-Since: Thu, 05 Oct 2017 03:58:38 GMT

请求消息有了变化,其中最值得关注的一点就是请求头If-Modified-Since

1
If-Modified-Since: Thu, 05 Oct 2017 03:58:38 GMT

这个时间正是第一次访问中,服务器给浏览器发过来响应头Last-Modified的时间,这是浏览器告诉服务器该已被缓存的资源的最后修改时间

响应消息

1
2
3
HTTP/1.1 304
ETag: W/"220-1507175918413"
Date: Thu, 05 Oct 2017 05:09:14 GMT

看到响应消息,比起第一次访问的响应消息,变化是巨大的。
这次的响应消息只有几行,最值得注意的是状态码304,对应的状态描述是Not Modified,这证明GET请求的资源在服务器端没有被修改
在这之后,如果我们不对静态资源a.html进行修改,那么每次访问的请求消息和响应消息都会是以上所示的这种结果。

变动后再进行访问

现在你可以随便对a.html进行改动,改动后再次访问,我们所看到的信息又会有所不同。

请求消息

1
2
3
4
5
6
7
8
9
10
11
12
13
GET /day04-http/a.html HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4
Cookie: JSESSIONID=A26AF7FA34CE430A21E32D8666B05717; Idea-8752b670=e6037236-ceb5-4357-9600-1e08fca1a591;
_ga=GA1.1.327730390.1505579288; _gid=GA1.1.181028957.1507174466
If-None-Match: W/"220-1507175918413"
If-Modified-Since: Thu, 05 Oct 2017 03:58:38 GMT

响应消息

1
2
3
4
5
6
7
HTTP/1.1 200
Accept-Ranges: bytes
ETag: W/"222-1507190102463"
Last-Modified: Thu, 05 Oct 2017 07:55:02 GMT
Content-Type: text/html
Content-Length: 222
Date: Thu, 05 Oct 2017 07:55:04 GMT

请求消息没什么太大的变动,我们应把集中力都放到响应消息中:
与第一次访问的没太大差异,但其中最重要的就是Last-Modified,因为服务器端的a.html内容变了,所以服务器向浏览器重新发送了最后修改时间。
如果在这之后再次访问a.html,现在就知道肯定是If-Modified-Since发生了变化,它的值会变成这次通过Last-Modified传递过来的时间。

内部原理

通过前面的情景,现在我们都知道Last-ModifiedLast-Modified-Since分别指的是什么:

  • Last-Modified指的是服务器端资源的最后修改时间
  • Last-Modified-Since指的是已被浏览器缓存的资源的最后修改时间

Last-ModifiedLast-Modified-Since正如以下描述那样通力合作来控制缓存的:

  1. 浏览器通过GET方式向服务器第一次请求资源,则该资源会被浏览器缓存起来
    同时服务器发送给浏览器的响应头Last-Modified会告知该资源的最后修改时间,浏览器也会把它缓存起来

  2. 在这之后,浏览器每一次通过GET方式向服务器请求该已被缓存的资源时
    浏览器会把它对应的已被缓存起来的最后修改时间通过请求头Last-Modified-Since发送给服务器

  3. 服务器接到浏览器对该已被缓存的资源的GET方式请求后
    首先是把服务器中对应实际资源的Last-Modified以及浏览器发过来的Last-Modified-Since进行对比
    通俗来说就是:服务器对服务器中对应实际资源的最后修改时间已被浏览器缓存的对应资源的最后修改时间两者进行对比

    • 如果两者的时间是一致,则证明在服务器上的对应实际资源没有改动,与被浏览器缓存的对应资源一致
      那么就返回状态码304,浏览器接收到这状态码就直接把之前缓存的内容重新拿出来显示

    • 如果两者的时间不一致,则明在服务器上的对应实际资源发生变动,与被浏览器缓存的对应资源不一致
      那么就返回状态码200,同时发送最新变动的资源以及通过响应头Last-Modified发送已变动资源的最后修改时间给浏览器
      最终,浏览器就会显示更新过的资源并且将它缓存起来,覆盖以前的缓存,对应的最后更新时间的缓存也同样会被重新覆盖

在HttpServlet中的应用

  之前我们一直访问的都是静态资源,静态资源的缓存好控制,默认就已经实现了,我们直接就可以拿来用。如果是Servlet这种动态资源,又如何通过Last-ModifiedLast-Modified-Since来控制缓存呢?
  我们都知道Servlet是属于服务器这一端的,所以我们只管控制Last-Modified即可。

查看源码

通过查看HttpServlet的源码就可以发现:
其实HttpServlet对于GET请求的处理的原理也和前面所说的那样差不多,同时默认就已经有控制Last-Modified的实现了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public abstract class HttpServlet extends GenericServlet {
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {

String method = req.getMethod();
// 获取请求资源在服务器上的最后修改时间
if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
// 默认的是-1表示不知道资源的最后修改时间
if (lastModified == -1) {
// servlet doesn't support if-modified-since, no reason
// to go through further expensive logic
doGet(req, resp);
} else {
// 从请求中获取浏览器缓存对应资源的最后修改时间
long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
// 如果浏览器缓存对应资源的最后修改时间 早于 最后修改时间, 则说明资源变动了, 需要重新发送内容
// 否则就发送状态码304, 即NOT MODIFIED
if (ifModifiedSince < lastModified) {
// If the servlet mod time is later, call doGet()
// Round down to the nearest second for a proper compare
// A ifModifiedSince of -1 will always be less
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}
}
}

/**
*
* Returns the time the <code>HttpServletRequest</code>
* object was last modified,
* in milliseconds since midnight January 1, 1970 GMT.
* If the time is unknown, this method returns a negative
* number (the default).
*
* 下面这一句告诉我们:
* 通过重写这个方法可以决定该Servlet的最后修改时间
*
* <p>Servlets that support HTTP GET requests and can quickly determine
* their last modification time should override this method.
* This makes browser and proxy caches work more effectively,
* reducing the load on server and network resources.
*
* @param req the <code>HttpServletRequest</code>
* object that is sent to the servlet
*
* @return a <code>long</code> integer specifying
* the time the <code>HttpServletRequest</code>
* object was last modified, in milliseconds
* since midnight, January 1, 1970 GMT, or
* -1 if the time is not known
*/
protected long getLastModified(HttpServletRequest req) {
return -1;
}
}

光从service方法处理GET请求的分支以及getLastModifed方法这两段源码就可以得知很多信息了。
所以,我们最终的目的就是在自己的HttpServlet里重写getLastModified方法即可。

实际代码样例

先自定义一个HttpServlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
* 通过存储和修改资源的最后修改时间对请求资源缓存进行控制
* - HTTP中经常用响应头last-modified来实现缓存的控制
*
* - 在Servlet中定义一个数据成员lastModified,
* 重写HttpServlet的getLastModified方法,
* 在方法内部根据自己的需求去更新lastModified
*
* @author plaYwiThsouL
*/
public class CacheServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 每次访问该Servlet都会输出当前时间
// 但显示的时间会被getLastModified方法所控制
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
resp.getWriter().write("<h1 style='color: #blue'>" + format.format(new Date()) + "</h1>");
resp.getWriter().close();
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}

// 最后修改日期
private long lastModified = System.currentTimeMillis();

// 该方法是用于提供本动态资源的最后修改时间
@Override
protected long getLastModified(HttpServletRequest req) {
long current = System.currentTimeMillis();
// 假设该Servlet的资源10秒变动一次
// 在实际开发中, 当然是根据实际情况来决定什么时候更新最后修改时间
if (current - lastModified > (10 * 1000)) {
lastModified = current;
}
return lastModified;
}
}

当你访问这个Servlet,并发现它所显示的时间是每隔10秒就变化1次的的证明缓存控制成功了!